Skip to content

fix(functions): keep partial iteratee arguments out of keyword-bound slots - #448

Merged
dgilland merged 2 commits into
dgilland:developfrom
Cyber-Marty:fix/partial-middle-keyword-argcount
Nov 11, 2026
Merged

dgilland merged 2 commits into
dgilland:developfrom
Cyber-Marty:fix/partial-middle-keyword-argcount

Conversation

@Cyber-Marty

Copy link
Copy Markdown
Contributor

What changed

Partial._argcount now ends positional capacity just before the first positional parameter bound by
keyword, for both partial and partial_right, and indexes those slots with the same
signature-based parameter list that _argcount counts, so bound methods, classmethods and callable
instances are not shifted by an implicit self/cls.

Symptom

import functools
import pydash as p


def transform(value, increment=0, scale=1):
    return (value + increment) * scale


wrapped = p.partial(transform, increment=2)
p.map_([1, 2], wrapped)
# TypeError: transform() got multiple values for argument 'increment'

p.map_([1, 2], functools.partial(transform, increment=2))  # [3, 4]

The wrapper itself is fine (wrapped(1) == 3); the failure only happens when pydash calls it as an
iteratee.

Root cause

_argcount subtracted the number of keyword-bound parameters from the positional parameter count,
which is only correct when the keywords bind the trailing parameters:

argcount = func_argcount - len(self.args) - bound_keywords  # 3 - 0 - 1 == 2

With increment=2 bound, a three-parameter function still reports an arity of 2, so callit
passes (element, index) and the index lands in the slot that increment already occupies.

Fix

Both corrections live in the same expression:

  • Positional capacity ends at the first keyword-bound slot:
    min(func_argcount - len(self.args), first_bound_slot - len(self.args)).
  • Slot indices come from argspec.args after skipping len(argspec.args) - func_argcount leading
    slots, because getfullargspec keeps an implicit self/cls that the signature-based
    func_argcount already excludes. Without that alignment a bound method reports one slot too many
    and re-raises TypeError for trailing keyword bindings, which develop currently handles.

Trailing keyword bindings keep their previous arity (partial(transform, scale=2)._argcount == 2),
keyword-only parameters and **kwargs still consume no positional slots, and callables without an
inspectable signature keep the previous subtractive behavior.

Tests

tests/test_functions.py gains ten cases, parameterized over [_.partial, _.partial_right] where
both directions agree:

  • the reported failure through map_ and chain(...).map_(), plus the direct call as a control;
  • arity for trailing, middle and double keyword bindings;
  • bound method, classmethod and callable instance, where the implicit first parameter must not shift
    the bound slot;
  • bound positional arguments combined with a keyword slot, including the zero-argument case;
  • a bound method through map_ for both the trailing binding (already working on develop) and the
    middle binding.

The reported TypeError and assert 2 == 1 fail on develop at 1cf4661. The bound-method cases
are preservation controls: develop already returns [2, 6] there, and they guard the implicit
self alignment added here. Existing test_partial_keyword_only_arguments_as_iteratee and
test_partial_variable_keyword_arguments_as_iteratee continue to pass.

Verification

  • pytest src/pydash tests --cov=pydash --ignore=tests/pytest_mypy_testing — 3322 passed, 100%
    coverage on CPython 3.13.15.
  • pytest tests/test_functions.py — 144 passed on CPython 3.10.21 and 3.14.9.
  • ruff format --check, ruff check, mypy src/pydash tests tasks.py — clean.
  • sphinx-build -q -W -b html docs docs/_build/html — clean.
  • 3.11 and 3.12 were not available locally; CI covers the full matrix.

Notes

This addresses the scope question in #416, where the maintainer replied that fixes and improvements
are welcomed. The change is limited to the arity calculation; no public signature, units, dtype or
output-shape behavior changes.

Prepared with AI agent assistance; every command and output above was executed locally against this
branch, and the patch was additionally reviewed by a second automated reviewer. No independent human
code review is claimed.

Cyber-Marty and others added 2 commits November 11, 2026 09:42
…slots

`Partial._argcount` subtracted every keyword binding from the positional count, so a
middle parameter bound by keyword still advertised a slot that a caller cannot fill.
`map_` then passed the index into that slot and raised
`TypeError: got multiple values for argument`.

Positional capacity now ends just before the first keyword-bound slot. Slot indices are
taken from `argspec.args` after skipping the implicit `self`/`cls` that `getfullargspec`
reports but the signature-based `func_argcount` already excludes, so bound methods,
classmethods and callable instances keep their previous arity for trailing bindings.

Keyword-only parameters, `**kwargs`, varargs-only callables and callables without an
inspectable signature keep their existing behavior.

Adds regression tests for both partial directions: the reported failure through `map_` and
`chain(...).map_()`, arity for trailing/middle/double bindings, implicit first parameters,
bound positional arguments with a keyword slot, and the zero-argument case.
@coveralls

coveralls commented Nov 11, 2026 •

Copy link
Copy Markdown

Coverage Status

coverage: 100.0%. remained the same — Cyber-Marty:fix/partial-middle-keyword-argcount into dgilland:develop

@dgilland
dgilland merged commit b6bd031 into dgilland:develop Nov 11, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants